eletrotupi / tcc/ frontend/app/entry/[id].tsx4e6e379351e597470bb29f5b250f14df1d6b74f5
11.9 KB | 412 lines Raw
1
import { useState, useEffect, use } from "react";
2
import {
3
  StyleSheet,
4
  Text,
5
  View,
6
  ScrollView,
7
  Pressable,
8
  ActivityIndicator,
9
} from "react-native";
10
import { useLocalSearchParams, router, Stack } from "expo-router";
11
import { format } from "date-fns";
12
import { ptBR } from "date-fns/locale";
13

14
import { Card, SubtleInfoCard } from "@/components/ui/Cards";
15
import { NoLinkMessage } from "@/components/misc/NoLinkMessage";
16
import { Badge } from "@/components/ui/Badge";
17
import { Button } from "@/components/ui/Button";
18
import { Col, Between, Row } from "@/components/ui/LayoutHelpers";
19
import { Section, SectionHeader } from "@/components/ui/Sections";
20
import { Theme, Colors, Spacing } from "@/constants/theme";
21
import { getMood } from "@/constants/moods";
22
import {
23
  getMoodComponent,
24
  intensityLabel,
25
  intensityToValue,
26
} from "@/constants/mood-components";
27
import { useMoodEntry, useDeleteMoodEntry } from "@/hooks";
28
import { getTimeBadge, formatMoment } from "@/lib/utils/time";
29
import { MoodComponentCard } from "@/components/ui/MoodComponentCard";
30
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
31
import { getTriggerCategory } from "@/constants/trigger-categories";
32

33
const impactColor = (n: number) => {
34
  if (n <= 1) return '#86efac';
35
  if (n <= 2) return '#22c55e';
36
  if (n <= 3) return '#f59e0b';
37
  if (n <= 4) return '#f97316';
38
  return '#ef4444';
39
};
40

41
export default function MoodDetailScreen() {
42
  const { id } = useLocalSearchParams<{ id: string }>();
43
  const { data: entry, isLoading, isError } = useMoodEntry(id);
44
  const deleteMoodEntry = useDeleteMoodEntry();
45
  const [minutesLeft, setMinutesLeft] = useState(0);
46
  const [canDelete, setCanDelete] = useState(false);
47

48
  useEffect(() => {
49
    const updateTimer = () => {
50
      const moment = new Date(entry?.moment);
51
      const createdAt = new Date(entry?.moment);
52
      const msSinceCreation = Date.now() - createdAt.getTime();
53
      const deletionWindowMs = 5 * 60 * 1000;
54

55
      const newCanDelete = msSinceCreation < deletionWindowMs;
56
      const newMinutesLeft = Math.max(
57
        0,
58
        Math.ceil((deletionWindowMs - msSinceCreation) / 60000),
59
      );
60

61
      setCanDelete(newCanDelete);
62
      setMinutesLeft(newMinutesLeft);
63
    };
64

65
    updateTimer(); // Set initial values
66

67
    const interval = setInterval(updateTimer, 1000);
68

69
    return () => clearInterval(interval);
70
  }, [entry?.moment]);
71

72
  if (isLoading) {
73
    return (
74
      <View style={styles.centered}>
75
        <ActivityIndicator size="large" color={Theme.colors.tint} />
76
      </View>
77
    );
78
  }
79

80
  if (isError || !entry) {
81
    return (
82
      <View style={styles.centered}>
83
        <Text style={styles.errorText}>Registro não encontrado.</Text>
84
        <Pressable onPress={() => router.back()} style={styles.backButton}>
85
          <Text style={styles.backButtonText}>Voltar</Text>
86
        </Pressable>
87
      </View>
88
    );
89
  }
90

91
  const moment = new Date(entry.moment);
92
  const moodDef = getMood(entry.selectedMood.toLowerCase());
93
  const badge = getTimeBadge(moment);
94

95
  const handleDelete = async () => {
96
    await deleteMoodEntry.mutateAsync(String(entry.id));
97

98
    router.back();
99
  };
100

101
  const deleteButtonMsg = () => {
102
    const msg = "Excluir Registro";
103
    const timeLeftStr = `(Disponível por ${minutesLeft < 1 ? "<1" : minutesLeft}:00)`;
104
    const suffix = canDelete ? timeLeftStr : "(Expirado)";
105

106
    return `${msg} ${suffix}`;
107
  };
108

109
  return (
110
    <>
111
      <Stack.Screen options={{ title: "Ver detalhes" }} />
112

113
      <ScrollView
114
        style={styles.container}
115
        contentContainerStyle={styles.content}
116
        showsVerticalScrollIndicator={false}
117
      >
118
        {/* Header card */}
119
        <Card style={styles.headerCard}>
120
          <Between>
121
            <Row gap={12} style={{ alignItems: "center" }}>
122
              <Text style={styles.moodIcon}>{moodDef?.icon ?? "😐"}</Text>
123
              <Col gap={2}>
124
                <Text style={styles.moodLabel}>
125
                  {moodDef?.label ?? entry.selectedMood}
126
                </Text>
127
                <Text style={styles.moodTime}>{formatMoment(moment)}</Text>
128
              </Col>
129
            </Row>
130
            {/* If this is the latest mood entry, show the active badge */}
131
            <Badge
132
              label="Registro Ativo"
133
              backgroundColor={Theme.colors.activeBackground}
134
              color={Theme.colors.tint}
135
            />
136
          </Between>
137
        </Card>
138

139
        {/* Core levels */}
140
        <Section>
141
          <SectionHeader title="Níveis emocionais" variant="subtle" />
142
          {[
143
            { id: "energy", label: "Energia", value: entry.energyLevel },
144
            { id: "stress", label: "Estresse", value: entry.stressLevel },
145
            { id: "anxiety", label: "Ansiedade", value: entry.anxietyLevel },
146
          ].map((item, index, arr) => (
147
            <Card key={item.id} style={{ padding: Spacing.cardGap }}>
148
              <View key={item.label}>
149
                <View style={styles.componentRow}>
150
                  <Text style={styles.componentLabel}>{item.label}</Text>
151
                  <Text style={styles.componentIntensityGreen}>
152
                    {item.value}/10
153
                  </Text>
154
                </View>
155

156
                <View style={styles.progressTrack}>
157
                  <View
158
                    style={[
159
                      styles.progressFill,
160
                      { width: `${(item.value / 10) * 100}%` },
161
                    ]}
162
                  />
163
                </View>
164
              </View>
165
            </Card>
166
          ))}
167
        </Section>
168

169
        <Section>
170
          <SectionHeader title="Além disso, sinto..." variant="subtle" />
171
          <Col gap={0}>
172
            {entry.moodComponents.map((comp, index) => {
173
              const def = getMoodComponent(comp.component.toLowerCase());
174
              const numericIntensity =
175
                comp.intensity === "LIGHT"
176
                  ? 2
177
                  : comp.intensity === "MODERATE"
178
                    ? 5
179
                    : 8;
180

181
              return (
182
                <MoodComponentCard
183
                  key={comp.id}
184
                  id={comp.component.toLowerCase()}
185
                  intensity={comp.intensity}
186
                />
187
              );
188
            })}
189
          </Col>
190
        </Section>
191

192
        {/* Linked triggers */}
193
        <Section>
194
          <SectionHeader title="Vínculo" variant="subtle" />
195
          {entry.triggerLinks && entry.triggerLinks.length > 0 ? (
196
            <Col gap={8}>
197
              {entry.triggerLinks.map((link) => {
198
                const cat = getTriggerCategory(link.trigger.category);
199
                return (
200
                  <Pressable
201
                    key={link.id}
202
                    onPress={() => router.push(`/triggers/${link.trigger.id}`)}
203
                    style={({ pressed }) => [pressed && { opacity: 0.7 }]}
204
                  >
205
                    <Card style={styles.linkCard}>
206
                      <Row style={{ alignItems: "center", gap: 12 }}>
207
                        <View style={[styles.linkIconCircle, { backgroundColor: cat.iconBackground }]}>
208
                          <MaterialIcons name={cat.icon} size={22} color={cat.color} />
209
                        </View>
210
                        <Col gap={3} style={{ flex: 1 }}>
211
                          <Row style={{ alignItems: "center", gap: 6 }}>
212
                            <Text style={styles.linkLabel}>{cat.label}</Text>
213
                            <Badge label="Gatilho" variant="orange" style={undefined} />
214
                          </Row>
215
                          <Text style={styles.linkTime}>
216
                            {formatMoment(new Date(link.trigger.moment))}
217
                          </Text>
218
                        </Col>
219
                        <Ionicons name="chevron-forward" size={16} color={Colors.light.disabled} />
220
                      </Row>
221
                      <View style={styles.impactBar}>
222
                        <View
223
                          style={[
224
                            styles.impactFill,
225
                            {
226
                              width: `${(link.perceivedImpact / 5) * 100}%` as any,
227
                              backgroundColor: impactColor(link.perceivedImpact),
228
                            },
229
                          ]}
230
                        />
231
                      </View>
232
                    </Card>
233
                  </Pressable>
234
                );
235
              })}
236
            </Col>
237
          ) : (
238
            <NoLinkMessage />
239
          )}
240
        </Section>
241

242
        {/* Annotation / notes */}
243
        {entry.annotation && (
244
          <Section>
245
            <SectionHeader title="Anotações" variant="subtle" />
246
            <Card style={{ padding: Spacing.cardGap }}>
247
              <Text style={styles.annotation}>
248
                "{entry.annotation?.trim()}"
249
              </Text>
250
            </Card>
251
          </Section>
252
        )}
253

254
        {/* Delete */}
255
        <View style={styles.section}>
256
          <Text style={styles.sectionLabel}>GESTÃO DO REGISTRO</Text>
257
          <Button
258
            variant="danger"
259
            isLoading={deleteMoodEntry.isPending}
260
            disabled={!canDelete || deleteMoodEntry.isPending}
261
            onPress={handleDelete}
262
            title={deleteButtonMsg()}
263
          />
264

265
          <SubtleInfoCard
266
            style={styles.deleteInfo}
267
            text={`
268
              A exclusão de registros é permitida apenas nos primeiros 5 minutos
269
              após o envio para garantir a integridade do histórico emocional.
270
              `}
271
          />
272
        </View>
273
      </ScrollView>
274
    </>
275
  );
276
}
277

278
const styles = StyleSheet.create({
279
  container: {
280
    flex: 1,
281
    backgroundColor: Theme.colors.background,
282
  },
283
  content: {
284
    padding: Spacing.containerPadding,
285
    gap: Spacing.sectionGap,
286
    paddingBottom: 48,
287
  },
288
  centered: {
289
    flex: 1,
290
    justifyContent: "center",
291
    alignItems: "center",
292
    gap: 12,
293
  },
294
  errorText: {
295
    fontSize: 15,
296
    color: Colors.light.textSecondary,
297
  },
298
  backButton: {
299
    paddingHorizontal: 20,
300
    paddingVertical: 10,
301
    backgroundColor: Theme.colors.tint,
302
    borderRadius: Theme.borderRadius.md,
303
  },
304
  backButtonText: {
305
    fontWeight: "700",
306
    color: Theme.colors.text,
307
  },
308

309
  // Header
310
  headerCard: {
311
    padding: 16,
312
  },
313
  moodIcon: {
314
    fontSize: 40,
315
  },
316
  moodLabel: {
317
    fontSize: 17,
318
    fontWeight: "700",
319
    color: Colors.light.text,
320
  },
321
  moodTime: {
322
    fontSize: 13,
323
    color: Colors.light.textSecondary,
324
  },
325

326
  // Sections
327
  section: {
328
    gap: 8,
329
  },
330
  sectionLabel: {
331
    fontSize: 11,
332
    fontWeight: "600",
333
    letterSpacing: 0.8,
334
    color: Colors.light.textSecondary,
335
    textTransform: "uppercase",
336
  },
337

338
  // Components
339
  componentRow: {
340
    flexDirection: "row",
341
    justifyContent: "space-between",
342
    alignItems: "center",
343
    paddingVertical: 10,
344
  },
345
  componentLabel: {
346
    fontSize: 15,
347
    fontWeight: "700",
348
    color: Colors.light.text,
349
  },
350
  componentIntensity: {
351
    fontSize: 13,
352
    color: Colors.light.textSecondary,
353
  },
354
  componentIntensityGreen: {
355
    fontSize: 13,
356
    fontWeight: "700",
357
    color: Theme.colors.tint,
358
  },
359
  progressTrack: {
360
    height: 6,
361
    backgroundColor: Theme.colors.light.divider,
362
    borderRadius: Theme.borderRadius.full,
363
    marginBottom: 4,
364
    overflow: "hidden",
365
  },
366
  progressFill: {
367
    height: "100%",
368
    backgroundColor: Theme.colors.light.tint,
369
    borderRadius: Theme.borderRadius.full,
370
  },
371
  // Annotation
372
  annotation: {
373
    fontSize: 14,
374
    lineHeight: 22,
375
    color: Colors.light.textSecondary,
376
    fontStyle: "italic",
377
    padding: 4,
378
  },
379
  // Linked trigger cards
380
  linkCard: {
381
    padding: Spacing.cardGap,
382
    overflow: "hidden",
383
    gap: 10,
384
  },
385
  linkIconCircle: {
386
    width: 44,
387
    height: 44,
388
    borderRadius: 22,
389
    alignItems: "center",
390
    justifyContent: "center",
391
  },
392
  linkTime: {
393
    fontSize: 12,
394
    color: Colors.light.textSecondary,
395
  },
396
  linkLabel: {
397
    fontSize: 15,
398
    fontWeight: "700",
399
    color: Colors.light.text,
400
  },
401
  impactBar: {
402
    height: 3,
403
    backgroundColor: Colors.light.divider,
404
    borderRadius: 99,
405
    overflow: "hidden",
406
  },
407
  impactFill: {
408
    height: "100%",
409
    borderRadius: 99,
410
  },
411

412
});